const http = require('http')
const fs = require('fs').promises
const path = require('path')
const url = require('url')
const os = require('os')
const chalk = require('chalk')
const mime = require('mime')
const zlib = require('zlib')
const crypto = require('crypto')
const { createReadStream, readFileSync } = require('fs')
const { render } = require('./readFile')
// 一般是 强缓存 + 协商缓存

// 代码启动时 直接读取模板，制度去一次即可
const template = readFileSync(path.resolve(__dirname, 'tmpl.html'), 'utf8')

// 获取本机 ip 地址
const address = Object.values(os.networkInterfaces()).flat().find(item => item.family === 'IPv4').address

module.exports = class Server {
  constructor(options = {}) {
    this.port = options.port || 8080
    this.directory = options.directory
    this.address = address
    this.template = template
  }

  // es7 class 中使用 箭头函数
  handleRequest = async (req, res) => {
    // 监控请求的路径   看路径是否是文件，如果是文件直接返回。不是文件则读取文件的目录
    let { pathname } = url.parse(req.url)
    // 防止前端访问中文目录
    pathname = decodeURIComponent(pathname)
    // 不能使用 __dirname 路径 包含src。  当前执行目录下查找
    let filePath = path.join(this.directory, pathname)
    try {
      // 该 文件的状态信息
      let statObj = await fs.stat(filePath)
      if (statObj.isDirectory()) {
        let dirs = await fs.readdir(filePath)
        // 模板引擎  渲染模板
        // 实现原理  with  new Function

        let content = await render(template, { dirs:dirs.map(dir => {
          return {
            dir,
            // 文件夹内的文件再次访问，需要拼接，否则读取的是根目录下的地址
            url: path.join(pathname, dir)
          }
        })})
        res.setHeader('Content-Type', 'text/html;charset=utf-8')
        res.end(content)
      } else {
        // file
        // fs.readFile // 读完才显示

        this.sendFile(req, res, statObj, filePath)
      }
    } catch (error) {
      // 监听 await 异常  文件不存在
      this.sendError(res, error)
    }
  }
  // 对文件进行压缩，浏览器会自动解压。 配置 content-encoding
  gzip(req, res) {
/**
     * 发送前做压缩处理
     * node 默认支持 gzip deflate  nginx 默认 br压缩算法
     */
    // 默认做了处理，转为小写
    let encoding = req.headers['accept-encoding']
    // 流的方式 处理压缩
    let zip;
    if (encoding) {
      // 支持压缩
      let ways = encoding.split(', ')
      for(let i = 0; i<ways.length;i++) {
        let lib = ways[i]
        if (lib == 'gzip') {
          // 返回必须要设置头， 否则会下载zip
          res.setHeader('content-encoding', 'gzip')
          zip = zlib.createGzip()
          break
        } else if (lib == 'deflate'){
          res.setHeader('content-encoding', 'deflate')
          zip = zlib.createDeflate()
          break
        }
      }
    }

    return zip
  }

  // 使用 缓存。只对链接的资源文件有效，对 url 直接输入的无效
  cache(req, res, statObj, filePath) {
    res.setHeader('Cache-Control', 'no-cache') // 每次都会请求，缓存中有
    // 发送文件前，做缓存。 只针对引用的资源，url 直接访问的不会缓存。
    // 首次访问的资源不会被 缓存
    // 一般情况下 都设置  兼容
    
    // res.setHeader('Cache-Control', 'max-age=10') // s 单位
    // res.setHeader('Expires', new Date(Date.now() + 10 * 1000).toGMTString())

    // 有的文件 10s 后还是没有变，（对比文件，文件没变就接着走缓存）
    
    // last-modified  最后修改时间  服务器 给 客户端
    // if-modified-since  浏览器 访问时 带给服务端
    // 只要修改时间变了就会变，无关内容，（改来改去） 或者1s 内多次变化 也监听不到
    const ifModifiedSince = req.headers['if-modified-since']
    // 第一次 没有该值
    const ctime = (statObj.ctime).toGMTString()  // 读取的文件状态，包括修改时间
    res.setHeader('last-modified', ctime)
    if (ifModifiedSince !== ctime) {
      // 时间相同 代表 有缓存
      return false
    }

    // etag  根据内容生成 唯一标识。·拿到摘要 不能反解
    // md5 不可逆的。相同的内容 md5 值是一样的。  撞库可以获得原来的结果。 但不是反解
    /**
     * MD5 特点
     * 1. 不可逆
     * 2. 相同内容出来的结果相同
     * 3. 不同内容 结果完全不同
     * 4. 摘要后的长度  一致
     * crypto 库
     */

    // 服务器需要提供一个 etag，浏览器提供一个  if-none-match
    // 消耗性能
    const ifNoneMatch = req.headers['if-none-match']
    // fs.readFileSync(filePath)  读取的buffer        生成etag 的规则
    let etag = crypto.createHash('md5').update(readFileSync(filePath)).digest('base64') // 生成新的 base64
    res.setHeader('Etag', etag)
    if (ifNoneMatch != etag) {
      return false
    }
    return true
  }

  sendFile(req, res, statObj, filePath) {
    if (this.cache(req, res, statObj, filePath)) {
      res.statusCode = 304
      res.end()
      return
    }

    // 使用 流的方式
    /**
     * createReadStream(filename).on('data', function(chunk){
     * res.write(chunk)
     * res.end(chunk)
     * })
     * Transfer-Encoding: chunked 分片形式
     */
    res.setHeader('Content-Type', (mime.getType(filePath) || 'text/plain') + ';charset=utf-8') // 最好加 -
    // 支持压缩
    let zip = this.gzip(req, res)
    if (zip) {
      // 转换流
      createReadStream(filePath).pipe(zip).pipe(res)
    } else {
      createReadStream(filePath).pipe(res)// 直接返回  res.write  res.end
    }
  }

  sendError(res, e) {
    console.log('err', e)
    // 不设置状态码的话  返回 200，请求成功
    res.statusCode = 404
    res.end('NOT Found')
  }

  // 启动服务
  start() {
    const server = http.createServer(this.handleRequest)
    server.listen(this.port, () => {
      // path.relative
      console.log(`${chalk.yellow('Starting up day-server, serving:')} ${this.directory}`)
      console.log(`  http://${address}:${chalk.green(this.port)}`)
      console.log((`  http:127.0.0.1:${chalk.green(this.port)}`))
    })
  }
}